قدرت کلاسهای پایه انتزاعی (ABC) پایتون را آزاد کنید. تفاوت حیاتی بین تایپدهی ساختاری مبتنی بر پروتکل و طراحی رابط رسمی را بیاموزید.
کلاسهای پایه انتزاعی پایتون: تسلط بر پیادهسازی پروتکل در مقابل طراحی رابط
در دنیای توسعه نرمافزار، هدف نهایی ساختن برنامههایی است که قوی، قابل نگهداری و مقیاسپذیر باشند. با رشد پروژهها از چند اسکریپت ساده به سیستمهای پیچیدهای که توسط تیمهای بینالمللی مدیریت میشوند، نیاز به ساختار واضح و قراردادهای قابل پیشبینی اهمیت فوقالعادهای پیدا میکند. چگونه میتوانیم اطمینان حاصل کنیم که اجزای مختلف، که ممکن است توسط توسعهدهندگان مختلف در مناطق زمانی متفاوت نوشته شده باشند، میتوانند به صورت یکپارچه و قابل اعتماد با یکدیگر تعامل داشته باشند؟ پاسخ در اصل انتزاع (abstraction) نهفته است.
پایتون، با ماهیت پویای خود، فلسفه معروفی برای انتزاع دارد: «داک تایپینگ» (duck typing). اگر یک شیء مانند یک اردک راه برود و مانند یک اردک کواک کند، ما آن را به عنوان یک اردک در نظر میگیریم. این انعطافپذیری یکی از بزرگترین نقاط قوت پایتون است که توسعه سریع و کد تمیز و خوانا را ترویج میکند. با این حال، در برنامههای بزرگمقیاس، اتکای صرف به توافقات ضمنی میتواند منجر به باگهای نامحسوس و مشکلات نگهداری شود. چه اتفاقی میافتد وقتی یک «اردک» به طور غیرمنتظرهای نتواند پرواز کند؟ اینجاست که کلاسهای پایه انتزاعی (Abstract Base Classes - ABCs) پایتون وارد صحنه میشوند و مکانیزم قدرتمندی برای ایجاد قراردادهای رسمی بدون قربانی کردن روح پویای پایتون فراهم میکنند.
اما در اینجا یک تمایز حیاتی و اغلب اشتباه درک شده وجود دارد. ABCها در پایتون ابزاری یکسان برای همه موارد نیستند. آنها به دو فلسفه متمایز و قدرتمند طراحی نرمافزار خدمت میکنند: ایجاد رابطهای (interfaces) صریح و رسمی که نیازمند وراثت هستند، و تعریف پروتکلهای (protocols) انعطافپذیر که قابلیتها را بررسی میکنند. درک تفاوت بین این دو رویکرد — طراحی رابط در مقابل پیادهسازی پروتکل — کلید گشودن پتانسیل کامل طراحی شیءگرا در پایتون و نوشتن کدی است که هم انعطافپذیر و هم امن باشد. این راهنما هر دو فلسفه را بررسی کرده و نمونههای عملی و راهنماییهای روشنی برای زمان استفاده از هر رویکرد در پروژههای نرمافزاری جهانی شما ارائه میدهد.
نکتهای در مورد قالببندی: برای رعایت محدودیتهای قالببندی خاص، نمونههای کد در این مقاله با استفاده از استایلهای بولد و ایتالیک در تگهای متنی استاندارد ارائه شدهاند. توصیه میکنیم برای بهترین خوانایی، آنها را در ویرایشگر خود کپی کنید.
پایه و اساس: کلاسهای پایه انتزاعی دقیقاً چه هستند؟
قبل از پرداختن به دو فلسفه طراحی، بیایید یک پایه محکم ایجاد کنیم. یک کلاس پایه انتزاعی چیست؟ در هسته خود، یک ABC یک طرح اولیه برای کلاسهای دیگر است. این کلاس مجموعهای از متدها و ویژگیهایی را تعریف میکند که هر زیرکلاس منطبق باید پیادهسازی کند. این روشی برای گفتن این است که، «هر کلاسی که ادعا میکند بخشی از این خانواده است، باید این قابلیتهای خاص را داشته باشد.»
ماژول داخلی `abc` در پایتون ابزارهای لازم برای ایجاد ABCها را فراهم میکند. دو جزء اصلی عبارتند از:
- `ABC`: یک کلاس کمکی که به عنوان یک متاکلاس برای ایجاد یک ABC استفاده میشود. در پایتون مدرن (+3.4)، میتوانید به سادگی از `abc.ABC` ارثبری کنید.
- `@abstractmethod`: یک دکوراتور که برای علامتگذاری متدها به عنوان انتزاعی استفاده میشود. هر زیرکلاس از ABC باید این متدها را پیادهسازی کند.
دو قانون اساسی بر ABCها حاکم است:
- شما نمیتوانید یک نمونه (instance) از یک ABC بسازید که متدهای انتزاعی پیادهسازی نشده داشته باشد. این یک الگو است، نه یک محصول نهایی.
- هر زیرکلاس کانکریت (concrete) باید تمام متدهای انتزاعی به ارث برده شده را پیادهسازی کند. اگر این کار را انجام ندهد، آن نیز به یک کلاس انتزاعی تبدیل میشود و شما نمیتوانید از آن نمونهای بسازید.
بیایید این را با یک مثال کلاسیک در عمل ببینیم: سیستمی برای مدیریت فایلهای رسانهای.
مثال: یک ABC ساده برای MediaFile
تصور کنید در حال ساخت برنامهای هستیم که نیاز به مدیریت انواع مختلف رسانه دارد. ما میدانیم که هر فایل رسانهای، صرف نظر از فرمت آن، باید قابل پخش باشد و دارای مقداری فراداده (metadata) باشد. میتوانیم این قرارداد را با یک ABC تعریف کنیم.
import abc
class MediaFile(abc.ABC):
def __init__(self, filepath: str):
self.filepath = filepath
print(f"Base init for {self.filepath}")
@abc.abstractmethod
def play(self) -> None:
"""فایل رسانهای را پخش میکند."""
raise NotImplementedError
@abc.abstractmethod
def get_metadata(self) -> dict:
"""یک دیکشنری از فراداده رسانه را برمیگرداند."""
raise NotImplementedError
اگر سعی کنیم مستقیماً یک نمونه از `MediaFile` ایجاد کنیم، پایتون جلوی ما را خواهد گرفت:
# این کد یک TypeError ایجاد میکند
# media = MediaFile("path/to/somefile.txt")
# TypeError: Can't instantiate abstract class MediaFile with abstract methods get_metadata, play
برای استفاده از این طرح اولیه، باید زیرکلاسهای کانکریتی ایجاد کنیم که پیادهسازیهایی برای `play()` و `get_metadata()` ارائه دهند.
class AudioFile(MediaFile):
def play(self) -> None:
print(f"Playing audio from {self.filepath}...")
def get_metadata(self) -> dict:
return {"codec": "mp3", "duration_seconds": 180}
class VideoFile(MediaFile):
def play(self) -> None:
print(f"Playing video from {self.filepath}...")
def get_metadata(self) -> dict:
return {"codec": "h264", "resolution": "1920x1080"}
اکنون، میتوانیم نمونههایی از `AudioFile` و `VideoFile` ایجاد کنیم زیرا آنها قراردادی را که توسط `MediaFile` تعریف شده است، برآورده میکنند. این مکانیزم اساسی ABCها است. اما قدرت واقعی از *نحوه* استفاده ما از این مکانیزم ناشی میشود.
فلسفه اول: ABCها به عنوان طراحی رابط رسمی (تایپدهی اسمی)
اولین و سنتیترین روش استفاده از ABCها برای طراحی رابط رسمی است. این رویکرد ریشه در تایپدهی اسمی (nominal typing) دارد، مفهومی که برای توسعهدهندگانی که از زبانهایی مانند جاوا، C++ یا C# میآیند، آشناست. در یک سیستم اسمی، سازگاری یک نوع بر اساس نام و اعلان صریح آن تعیین میشود. در زمینه ما، یک کلاس تنها در صورتی یک `MediaFile` محسوب میشود که به صراحت از `MediaFile` ABC ارثبری کند.
به آن مانند یک گواهینامه حرفهای فکر کنید. برای اینکه یک مدیر پروژه معتبر باشید، نمیتوانید فقط مانند یک مدیر پروژه رفتار کنید؛ باید مطالعه کنید، یک آزمون خاص را بگذرانید و یک گواهینامه رسمی دریافت کنید که صراحتاً صلاحیت شما را بیان میکند. نام و تبار گواهینامه شما اهمیت دارد.
در این مدل، ABC به عنوان یک قرارداد غیرقابل مذاکره عمل میکند. با ارثبری از آن، یک کلاس یک وعده رسمی به بقیه سیستم میدهد که عملکرد مورد نیاز را ارائه خواهد داد.
مثال: یک چارچوب برای صادرکننده داده
تصور کنید در حال ساخت یک چارچوب هستیم که به کاربران اجازه میدهد دادهها را به فرمتهای مختلف صادر کنند. ما میخواهیم اطمینان حاصل کنیم که هر پلاگین صادرکننده از یک ساختار سختگیرانه پیروی میکند. میتوانیم یک رابط `DataExporter` تعریف کنیم.
import abc
from datetime import datetime
class DataExporter(abc.ABC):
"""یک رابط رسمی برای کلاسهای صادرکننده داده."""
@abc.abstractmethod
def export(self, data: list[dict]) -> str:
"""دادهها را صادر کرده و یک پیام وضعیت برمیگرداند."""
pass
def get_timestamp(self) -> str:
"""یک متد کمکی کانکریت که توسط همه زیرکلاسها به اشتراک گذاشته شده است."""
return datetime.utcnow().isoformat()
class CSVExporter(DataExporter):
def export(self, data: list[dict]) -> str:
filename = f"export_{self.get_timestamp()}.csv"
print(f"Exporting {len(data)} rows to {filename}")
# ... منطق واقعی نوشتن CSV ...
return f"Successfully exported to {filename}"
class JSONExporter(DataExporter):
def export(self, data: list[dict]) -> str:
filename = f"export_{self.get_timestamp()}.json"
print(f"Exporting {len(data)} records to {filename}")
# ... منطق واقعی نوشتن JSON ...
return f"Successfully exported to {filename}"
در اینجا، `CSVExporter` و `JSONExporter` به صراحت و به طور قابل تأیید `DataExporter` هستند. منطق اصلی برنامه ما میتواند با اطمینان به این قرارداد تکیه کند:
def run_export_process(exporter: DataExporter, data_to_export: list[dict]):
print("--- Starting export process ---")
if not isinstance(exporter, DataExporter):
raise TypeError("Exporter must be a valid DataExporter implementation.")
status = exporter.export(data_to_export)
print(f"Process finished with status: {status}")
# Usage
data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
run_export_process(CSVExporter(), data)
run_export_process(JSONExporter(), data)
توجه داشته باشید که ABC همچنین یک متد کانکریت، `get_timestamp()`، را فراهم میکند که عملکرد مشترکی را به تمام فرزندان خود ارائه میدهد. این یک الگوی رایج و قدرتمند در طراحی مبتنی بر رابط است.
مزایا و معایب رویکرد رابط رسمی
مزایا:
- بدون ابهام و صریح: قرارداد کاملاً روشن است. یک توسعهدهنده میتواند خط وراثت `class CSVExporter(DataExporter):` را ببیند و بلافاصله نقش و قابلیتهای کلاس را درک کند.
- سازگار با ابزارها: IDEها، لینترها و ابزارهای تحلیل استاتیک به راحتی میتوانند قرارداد را تأیید کنند و تکمیل خودکار و بررسی خطای عالی را ارائه دهند.
- عملکرد مشترک: ABCها میتوانند متدهای کانکریت ارائه دهند، به عنوان یک کلاس پایه واقعی عمل کنند و تکرار کد را کاهش دهند.
- آشنا بودن: این الگو برای توسعهدهندگان از اکثریت قریب به اتفاق سایر زبانهای شیءگرا فوراً قابل تشخیص است.
معایب:
- اتصال محکم (Tight Coupling): کلاس کانکریت اکنون مستقیماً به ABC گره خورده است. اگر ABC نیاز به جابجایی یا تغییر داشته باشد، همه زیرکلاسها تحت تأثیر قرار میگیرند.
- صلبیت: این رویکرد یک رابطه سلسله مراتبی سخت را تحمیل میکند. چه میشود اگر یک کلاس بتواند منطقاً به عنوان یک صادرکننده عمل کند اما از قبل از یک کلاس پایه متفاوت و ضروری ارثبری کرده باشد؟ وراثت چندگانه پایتون میتواند این مشکل را حل کند، اما میتواند پیچیدگیهای خاص خود را نیز به همراه داشته باشد (مانند مشکل الماس - Diamond Problem).
- تهاجمی بودن: نمیتوان از آن برای سازگار کردن کدهای شخص ثالث استفاده کرد. اگر از کتابخانهای استفاده میکنید که کلاسی با متد `export()` ارائه میدهد، نمیتوانید آن را به یک `DataExporter` تبدیل کنید مگر اینکه از آن زیرکلاس بسازید (که ممکن است ممکن یا مطلوب نباشد).
فلسفه دوم: ABCها به عنوان پیادهسازی پروتکل (تایپدهی ساختاری)
فلسفه دوم، که بیشتر «پایتونی» است، با داک تایپینگ همسو است. این رویکرد از تایپدهی ساختاری (structural typing) استفاده میکند، جایی که سازگاری نه بر اساس نام یا تبار، بلکه بر اساس ساختار و رفتار تعیین میشود. اگر یک شیء متدها و ویژگیهای لازم برای انجام کار را داشته باشد، نوع مناسب برای آن کار در نظر گرفته میشود، صرف نظر از سلسله مراتب کلاس اعلام شده آن.
به توانایی شنا کردن فکر کنید. برای اینکه یک شناگر در نظر گرفته شوید، نیازی به گواهینامه یا عضویت در شجرهنامه «شناگر» ندارید. اگر میتوانید خود را در آب به جلو برانید بدون اینکه غرق شوید، از نظر ساختاری یک شناگر هستید. یک انسان، یک سگ و یک اردک همگی میتوانند شناگر باشند.
از ABCها میتوان برای رسمی کردن این مفهوم استفاده کرد. به جای تحمیل وراثت، میتوانیم یک ABC تعریف کنیم که کلاسهای دیگر را به عنوان زیرکلاسهای مجازی خود به رسمیت بشناسد، اگر آنها پروتکل مورد نیاز را پیادهسازی کنند. این کار از طریق یک متد جادویی خاص انجام میشود: `__subclasshook__`.
وقتی شما `isinstance(obj, MyABC)` یا `issubclass(SomeClass, MyABC)` را فراخوانی میکنید، پایتون ابتدا وراثت صریح را بررسی میکند. اگر این بررسی ناموفق بود، سپس بررسی میکند که آیا `MyABC` متد `__subclasshook__` را دارد یا خیر. اگر داشته باشد، پایتون آن را فراخوانی میکند و میپرسد: «هی، آیا این کلاس را زیرکلاسی از خودت میدانی؟» این به ABC اجازه میدهد تا معیارهای عضویت خود را بر اساس ساختار تعریف کند.
مثال: یک پروتکل `Serializable`
بیایید یک پروتکل برای اشیائی تعریف کنیم که میتوانند به یک دیکشنری سریالایز شوند. ما نمیخواهیم هر شیء قابل سریالایز در سیستم ما مجبور به ارثبری از یک کلاس پایه مشترک باشد. آنها ممکن است مدلهای پایگاه داده، اشیاء انتقال داده یا کانتینرهای ساده باشند.
import abc
class Serializable(abc.ABC):
@abc.abstractmethod
def to_dict(self) -> dict:
pass
@classmethod
def __subclasshook__(cls, C):
if cls is Serializable:
# بررسی میکند که آیا 'to_dict' در ترتیب تفکیک متد C وجود دارد یا خیر
if any("to_dict" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
حالا، بیایید چند کلاس ایجاد کنیم. نکته مهم این است که هیچکدام از آنها از `Serializable` ارثبری نخواهند کرد.
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
def to_dict(self) -> dict:
return {"name": self.name, "email": self.email}
class Product:
def __init__(self, sku: str, price: float):
self.sku = sku
self.price = price
# این کلاس با پروتکل مطابقت ندارد
class Configuration:
def __init__(self, setting: str):
self.setting = setting
بیایید آنها را در برابر پروتکل خود بررسی کنیم:
print(f"Is User serializable? {isinstance(User('Test', 't@t.com'), Serializable)}")
print(f"Is Product serializable? {isinstance(Product('T-1000', 99.99), Serializable)}")
print(f"Is Configuration serializable? {isinstance(Configuration('ON'), Serializable)}")
# خروجی:
# Is User serializable? True
# Is Product serializable? False <- صبر کنید، چرا؟ بیایید این را اصلاح کنیم.
# Is Configuration serializable? False
آه، یک باگ جالب! کلاس `Product` ما متد `to_dict` را ندارد. بیایید آن را اضافه کنیم.
class Product:
def __init__(self, sku: str, price: float):
self.sku = sku
self.price = price
def to_dict(self) -> dict: # اضافه کردن متد
return {"sku": self.sku, "price": self.price}
print(f"Is Product now serializable? {isinstance(Product('T-1000', 99.99), Serializable)}")
# خروجی:
# Is Product now serializable? True
با اینکه `User` و `Product` هیچ کلاس والد مشترکی (به جز `object`) ندارند، سیستم ما میتواند با هر دوی آنها به عنوان `Serializable` رفتار کند زیرا آنها پروتکل را برآورده میکنند. این برای جداسازی (decoupling) فوقالعاده قدرتمند است.
مزایا و معایب رویکرد پروتکل
مزایا:
- حداکثر انعطافپذیری: اتصال بسیار سست (loose coupling) را ترویج میکند. اجزاء فقط به رفتار اهمیت میدهند، نه به تبار پیادهسازی.
- سازگاری: این رویکرد برای سازگار کردن کدهای موجود، به ویژه از کتابخانههای شخص ثالث، برای تطبیق با رابطهای سیستم شما بدون تغییر کد اصلی، عالی است.
- ترویج ترکیب (Composition): یک سبک طراحی را تشویق میکند که در آن اشیاء از قابلیتهای مستقل ساخته میشوند به جای اینکه از طریق درختان وراثت عمیق و صلب ایجاد شوند.
معایب:
- قرارداد ضمنی: رابطه بین یک کلاس و پروتکلی که پیادهسازی میکند از تعریف کلاس بلافاصله مشخص نیست. یک توسعهدهنده ممکن است نیاز داشته باشد کدبیس را جستجو کند تا بفهمد چرا با یک شیء `User` به عنوان `Serializable` رفتار میشود.
- سربار زمان اجرا (Runtime Overhead): بررسی `isinstance` میتواند کندتر باشد زیرا باید `__subclasshook__` را فراخوانی کرده و بررسیهایی را روی متدهای کلاس انجام دهد.
- پتانسیل پیچیدگی: منطق داخل `__subclasshook__` میتواند بسیار پیچیده شود اگر پروتکل شامل چندین متد، آرگومان یا نوع بازگشتی باشد.
سنتز مدرن: `typing.Protocol` و تحلیل استاتیک
با افزایش استفاده از پایتون در سیستمهای بزرگمقیاس، تمایل به تحلیل استاتیک بهتر نیز افزایش یافت. رویکرد `__subclasshook__` قدرتمند است اما صرفاً یک مکانیزم زمان اجرا است. چه میشد اگر میتوانستیم از مزایای تایپدهی ساختاری *قبل* از اجرای کد بهرهمند شویم؟
این منجر به معرفی `typing.Protocol` در PEP 544 شد. این ابزار یک روش استاندارد و زیبا برای تعریف پروتکلهایی ارائه میدهد که عمدتاً برای بررسیکنندههای نوع استاتیک مانند Mypy، Pyright یا بازرس PyCharm در نظر گرفته شدهاند.
یک کلاس `Protocol` شبیه به مثال `__subclasshook__` ما کار میکند اما بدون کد تکراری. شما به سادگی متدها و امضاهای آنها را تعریف میکنید. هر کلاسی که متدها و امضاهای منطبق داشته باشد، توسط یک بررسیکننده نوع استاتیک از نظر ساختاری سازگار در نظر گرفته میشود.
مثال: یک پروتکل `Quacker`
بیایید به مثال کلاسیک داک تایپینگ برگردیم، اما با ابزارهای مدرن.
from typing import Protocol
class Quacker(Protocol):
def quack(self, volume: int) -> str:
"""صدای کواک تولید میکند."""
... # توجه: بدنه متد پروتکل لازم نیست
class Duck:
def quack(self, volume: int) -> str:
return f"QUACK! (at volume {volume})"
class Dog:
def bark(self, volume: int) -> str:
return f"WOOF! (at volume {volume})"
def make_sound(animal: Quacker):
print(animal.quack(10))
make_sound(Duck()) # تحلیل استاتیک با موفقیت انجام میشود
make_sound(Dog()) # تحلیل استاتیک با شکست مواجه میشود!
اگر این کد را از طریق یک بررسیکننده نوع مانند Mypy اجرا کنید، خط `make_sound(Dog())` را با یک خطا علامتگذاری میکند: `Argument 1 to "make_sound" has incompatible type "Dog"; expected "Quacker"`. بررسیکننده نوع میفهمد که `Dog` پروتکل `Quacker` را برآورده نمیکند زیرا فاقد متد `quack` است. این خطا قبل از اجرای کد شناسایی میشود.
پروتکلهای زمان اجرا با `@runtime_checkable`
به طور پیشفرض، `typing.Protocol` فقط برای تحلیل استاتیک است. اگر سعی کنید از آن در یک بررسی `isinstance` در زمان اجرا استفاده کنید، با خطا مواجه خواهید شد.
# isinstance(Duck(), Quacker) # -> TypeError: Protocol 'Quacker' cannot be instantiated
با این حال، شما میتوانید با دکوراتور `@runtime_checkable` شکاف بین تحلیل استاتیک و رفتار زمان اجرا را پر کنید. این دکوراتور اساساً به پایتون میگوید که منطق `__subclasshook__` را به طور خودکار برای شما تولید کند.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Quacker(Protocol):
def quack(self, volume: int) -> str: ...
class Duck:
def quack(self, volume: int) -> str: return "..."
print(f"Is Duck an instance of Quacker? {isinstance(Duck(), Quacker)}")
# خروجی:
# Is Duck an instance of Quacker? True
این به شما بهترینهای هر دو دنیا را میدهد: تعاریف پروتکل تمیز و اعلانی برای تحلیل استاتیک، و گزینه اعتبارسنجی در زمان اجرا در صورت نیاز. با این حال، به خاطر داشته باشید که بررسیهای زمان اجرا روی پروتکلها کندتر از فراخوانیهای استاندارد `isinstance` هستند، بنابراین باید با احتیاط استفاده شوند.
تصمیمگیری عملی: راهنمای یک توسعهدهنده جهانی
خب، کدام رویکرد را باید انتخاب کنید؟ پاسخ کاملاً به مورد استفاده خاص شما بستگی دارد. در اینجا یک راهنمای عملی بر اساس سناریوهای رایج در پروژههای نرمافزاری بینالمللی آورده شده است.
سناریو ۱: ساخت یک معماری پلاگین برای یک محصول SaaS جهانی
شما در حال طراحی سیستمی هستید (مثلاً یک پلتفرم تجارت الکترونیک، یک CMS) که توسط توسعهدهندگان داخلی و شخص ثالث در سراسر جهان گسترش خواهد یافت. این پلاگینها باید به طور عمیق با برنامه اصلی شما یکپارچه شوند.
- توصیه: رابط رسمی (Nominal `abc.ABC`).
- دلیل: وضوح، پایداری و صراحت از اهمیت بالایی برخوردارند. شما به یک قرارداد غیرقابل مذاکره نیاز دارید که توسعهدهندگان پلاگین باید آگاهانه با ارثبری از `BasePlugin` ABC شما به آن پایبند باشند. این کار API شما را بدون ابهام میکند. همچنین میتوانید متدهای کمکی ضروری (مثلاً برای لاگگیری، دسترسی به پیکربندی، بینالمللیسازی) را در کلاس پایه ارائه دهید، که یک مزیت بزرگ برای اکوسیستم توسعهدهندگان شماست.
سناریو ۲: پردازش دادههای مالی از چندین API نامرتبط
برنامه فینتک شما باید دادههای تراکنش را از درگاههای پرداخت جهانی مختلف مصرف کند: Stripe، PayPal، Adyen و شاید یک ارائهدهنده منطقهای مانند Mercado Pago در آمریکای لاتین. اشیاء بازگردانده شده توسط SDKهای آنها کاملاً خارج از کنترل شما هستند.
- توصیه: پروتکل (`typing.Protocol`).
- دلیل: شما نمیتوانید کد منبع این SDKهای شخص ثالث را تغییر دهید تا آنها را وادار به ارثبری از کلاس پایه `Transaction` خود کنید. با این حال، میدانید که هر یک از اشیاء تراکنش آنها متدهایی مانند `get_id()`، `get_amount()` و `get_currency()` دارند، حتی اگر نام آنها کمی متفاوت باشد. شما میتوانید از الگوی Adapter به همراه یک `TransactionProtocol` برای ایجاد یک نمای یکپارچه استفاده کنید. یک پروتکل به شما اجازه میدهد *شکل* دادهای را که نیاز دارید تعریف کنید، و شما را قادر میسازد تا منطق پردازشی بنویسید که با هر منبع دادهای کار کند، تا زمانی که بتوان آن را برای تطبیق با پروتکل سازگار کرد.
سناریو ۳: بازسازی یک برنامه بزرگ و یکپارچه قدیمی (Monolithic Legacy)
شما موظف به تجزیه یک برنامه یکپارچه قدیمی به میکروسرویسهای مدرن هستید. کدبیس موجود شبکهای درهمتنیده از وابستگیها است و شما باید مرزهای مشخصی را بدون بازنویسی همه چیز به یکباره ایجاد کنید.
- توصیه: ترکیبی، اما با تکیه زیاد بر پروتکلها.
- دلیل: پروتکلها ابزاری استثنایی برای بازسازی تدریجی هستند. شما میتوانید با تعریف رابطهای ایدهآل بین سرویسهای جدید با استفاده از `typing.Protocol` شروع کنید. سپس، میتوانید آداپتورهایی برای بخشهایی از مونولیت بنویسید تا با این پروتکلها مطابقت داشته باشند بدون اینکه فوراً کد اصلی قدیمی را تغییر دهید. این به شما امکان میدهد تا اجزاء را به صورت تدریجی از هم جدا کنید. هنگامی که یک جزء کاملاً جدا شد و فقط از طریق پروتکل ارتباط برقرار کرد، آماده است تا به سرویس خود استخراج شود. ABCهای رسمی ممکن است بعداً برای تعریف مدلهای اصلی در سرویسهای جدید و تمیز استفاده شوند.
نتیجهگیری: بافتن انتزاع در کد شما
کلاسهای پایه انتزاعی پایتون گواهی بر طراحی عملگرایانه این زبان هستند. آنها یک جعبه ابزار پیچیده برای انتزاع فراهم میکنند که هم به انضباط ساختاریافته برنامهنویسی شیءگرای سنتی و هم به انعطافپذیری پویای داک تایپینگ احترام میگذارد.
سفر از یک توافق ضمنی به یک قرارداد رسمی، نشانه بلوغ یک کدبیس است. با درک دو فلسفه ABCها، میتوانید تصمیمات معماری آگاهانهای بگیرید که منجر به برنامههایی تمیزتر، قابل نگهداریتر و بسیار مقیاسپذیرتر میشود.
برای خلاصه کردن نکات کلیدی:
- طراحی رابط رسمی (تایپدهی اسمی): از `abc.ABC` با وراثت مستقیم زمانی استفاده کنید که به یک قرارداد صریح، بدون ابهام و قابل کشف نیاز دارید. این برای چارچوبها، سیستمهای پلاگین و موقعیتهایی که سلسله مراتب کلاس را کنترل میکنید، ایدهآل است. این مربوط به چیستی یک کلاس بر اساس اعلان آن است.
- پیادهسازی پروتکل (تایپدهی ساختاری): از `typing.Protocol` زمانی استفاده کنید که به انعطافپذیری، جداسازی و توانایی سازگار کردن کدهای موجود نیاز دارید. این برای کار با کتابخانههای خارجی، بازسازی سیستمهای قدیمی و طراحی برای چندریختی رفتاری عالی است. این مربوط به کاری است که یک کلاس میتواند انجام دهد بر اساس ساختار آن.
انتخاب بین یک رابط و یک پروتکل فقط یک جزئیات فنی نیست؛ این یک تصمیم طراحی بنیادی است که نحوه تکامل نرمافزار شما را شکل میدهد. با تسلط بر هر دو، خود را برای نوشتن کدی در پایتون مجهز میکنید که نه تنها قدرتمند و کارآمد، بلکه در برابر تغییر نیز زیبا و مقاوم است.